package org.swift.mashup.engine.model;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.swift.commons.xml.XmlUtils;
import org.swift.mashup.engine.InitializationException;
import org.swift.mashup.engine.api.ScriptEngineProvider;
import org.swift.mashup.engine.concurrency.agenda.Action;
import org.swift.mashup.engine.concurrency.agenda.ActionConfig;
import org.swift.mashup.engine.concurrency.agenda.ActionException;
import org.swift.mashup.engine.concurrency.agenda.ActionTemplate;
import org.swift.mashup.engine.concurrency.execution.Rule;
import org.swift.mashup.engine.concurrency.execution.RuleSet;
import org.swift.mashup.engine.util.XPathUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

public class MashupXmlActionConfig implements ActionConfig {
	private static final Logger logger = Logger.getLogger(MashupXmlActionConfig.class);
	
	public final static String NAMESPACE = XPathUtil.NAMESPACE;

	private Map<String, ActionTemplate> actionTemplates = new HashMap<String, ActionTemplate>();
	
	private RuleSet ruleSet = null;
	
	private boolean concurrent = false; 
	
	private boolean durable = true;

	private String documentation = null;

	private Integer agendaTimeout;
	
	private ActionTemplateParser<Node> parser;
	
	private Map<String,Object> inputParameters = new HashMap<String, Object>();
	
	private Set<ActionException> parseErrors = new HashSet<ActionException>();

	private Document schema;
	private Document validationRules	= null;

    private Integer rateLimit = null;

	public MashupXmlActionConfig () {
		parser = new MashupXmlActionTemplateParser();
	}
	
	public MashupXmlActionConfig (ScriptEngineProvider scriptEngineProvider){
		parser = new MashupXmlActionTemplateParser(scriptEngineProvider);
	}
	
	// temporary - not sure if this is called from anywhere yet
//	public void load(InputStream xml) throws ParserConfigurationException, SAXException, IOException, InitializationException, ParseException{
//		this.load(xml, null);
//	}
	
	public void load(InputStream xml,String mashupName) throws ParserConfigurationException, SAXException, 
			IOException, InitializationException, ParseException {
		Document doc = XmlUtils.toDocument(
			IOUtils.toString(xml));
		this.load(doc,mashupName);
	}
	
	// temporary - not sure if this is called from anywhere yet
//	public void load(Document document) throws InitializationException, ParseException{
//		this.load(document, null);
//	}

	public void load(Document document,String mashupName) throws InitializationException, ParseException {		
		// Initialize our rule set now
		RuleSet ruleSet =new RuleSet();
		setDocumentation(document);
		setConcurrent(document);
		setDurable(document);
		setSchema(document);
		setValidation(document);
        setRateLimit(document);
		try {
			// Select all with nodes that are on the mashup node and add
			// them as memory types to the rule set
			XPathExpression topWiths = createXPathExpression("/mx:mashup/mx:with", getContext());
			NodeList mashupInputParameters = (NodeList) topWiths.evaluate(document, XPathConstants.NODESET);
			logger.debug("Declaring " + mashupInputParameters.getLength() + " input parameters for this mashup.");
			for (int i = 0; i < mashupInputParameters.getLength(); i++){
				String withName = mashupInputParameters.item(i).getAttributes().getNamedItem("name").getTextContent();
				ruleSet.addMemoryType(withName);
				if (mashupInputParameters.item(i).getAttributes().getNamedItem("value") != null){
					String value =  mashupInputParameters.item(i).getAttributes().getNamedItem("value").getTextContent();
					ruleSet.getDefaultValues().put(withName,value);
				}
				this.inputParameters.put(withName, new Object());
				logger.debug("Added " + withName + " as an available input to this mashup.");
			}
			// Select all the return 'type' attributes and make them memory items as well
			XPathExpression returnTypes = createXPathExpression("//mx:return/@type", getContext());
			NodeList typeAttributes = (NodeList) returnTypes.evaluate(document, XPathConstants.NODESET);
			for (int i = 0; i < typeAttributes.getLength(); i++){
				Node currentTypeNode = typeAttributes.item(i);
				ruleSet.addMemoryType(currentTypeNode.getNodeValue());
			}
		} catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
		Element root = (Element)document.getElementsByTagNameNS(NAMESPACE, "mashup").item(0);
		NodeList nodes = root.getChildNodes();
		ActionTemplate actionTemplate;
		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);
			if (NAMESPACE.equals(node.getNamespaceURI())){
				if (this.parser.isParseable(node)){ 
					actionTemplate = this.parser.createActionTemplate(nodes.item(i));
					if (actionTemplate != null) {
						actionTemplate.setMashupName(mashupName);
						this.actionTemplates.put(actionTemplate.getName(), actionTemplate);
					}
				}
			}
		}
		addRulesToRuleSet(ruleSet, this.actionTemplates.values());
		setRuleSet(ruleSet);
	}

	public String getDocumentation() {
		return this.documentation;
	}

	private void setDocumentation(Document document) throws InitializationException {
		NodeList nodeList = document.getDocumentElement().getElementsByTagNameNS(NAMESPACE, "documentation");
		if(nodeList.getLength() == 0) {
			return;
		}

		try {
			Text documentationTextNode = (Text) nodeList.item(0).getFirstChild();
			this.documentation = documentationTextNode.getWholeText();
		} catch(Exception e) {
			throw new InitializationException(e);
		}
	}

	private void setValidation(Document document) throws InitializationException {
		logger.debug("setValidation(Document) - begin");
		NodeList	validationNodes	= null;
		Node		validationNode	= null;
		
		NodeList nodeList = document.getDocumentElement().getElementsByTagName("mx:validation");
		if ( nodeList != null && nodeList.getLength() > 0 && nodeList.item(0).hasChildNodes() ) {
			validationNodes	= nodeList.item(0).getChildNodes();
				
			for ( int x=0; x <  validationNodes.getLength(); x++ ) {
				if ( validationNodes.item(x).getNodeType() == Node.ELEMENT_NODE ) {
					logger.info("setValidation(Document) - validationNodes(" + x + ")=" + validationNodes.item(x).getNodeName() );
					validationNode	= validationNodes.item(x);
					break;
				}
			}
			
			if ( validationNode != null ) {
				try {
					this.validationRules = XmlUtils.toDocument(validationNode);
					logger.info("setValidation(Document) - this.validationRules=" + this.validationRules.toString());
				} catch(Exception e) {
					throw new InitializationException(e);
				}
			}
		}
		
		logger.debug("setValidation(Document) - end");
	}
	
	private void setSchema(Document document) throws InitializationException {
		NodeList nodeList = document.getDocumentElement().getElementsByTagName("xs:schema");
		if(nodeList.getLength() == 0) {
			return;
		}

		try {
			this.schema = XmlUtils.toDocument(nodeList.item(0));
		} catch(Exception e) {
			throw new InitializationException(e);
		}
	}

	private void setConcurrent(Document document) {
		this.concurrent = "true".equals(document.getDocumentElement().getAttribute("concurrent"));
	}

	public boolean isDurable() {
		return this.durable;
	}

	public void setDurable(boolean durable) {
		this.durable = durable;
	}

	private void setDurable(Document document) {
		String attrValue = document.getDocumentElement().getAttribute("durable");
		this.durable = !"false".equals(attrValue);
	}

	public Integer getAgendaTimeout() {
		return this.agendaTimeout;
	}

	public void setAgendaTimeout(int timeout) {
		this.agendaTimeout = timeout;
	}

	private void setAgendaTimeout(Document document) {
		String attrValue = document.getDocumentElement().getAttribute("agendaTimeout");
		try {
			if(!StringUtils.isEmpty(attrValue)) {
				Integer timeout = Integer.parseInt(attrValue);
				this.agendaTimeout = timeout;
			}
		}
		catch(NumberFormatException e) {
			logger.error("Couldn't parse agendaTimeout value " + attrValue, e);
		}
	}

	public Action createAction(String key, Map<String, ? extends Object> parameters) {
		return this.actionTemplates.get(key).newAction(parameters);
	}
	
	protected void addRulesToRuleSet (RuleSet ruleSet, Collection<ActionTemplate> actionTemplates){
		for (ActionTemplate actionTemplate : actionTemplates){
			Rule rule = new Rule( actionTemplate.getName() );
			rule.getActions().add(actionTemplate);
			if (!actionTemplate.getFrom().getSource().startsWith("http") && "" != actionTemplate.getFrom().getSource()){
				rule.addParameter(actionTemplate.getFrom().getSource(), actionTemplate.getFrom().getSource());
			}
			for (Entry<String, With> with : actionTemplate.getWiths().entrySet()){
				rule.addParameter(with.getKey(), with.getValue().getFrom());
			}
			for (When when : actionTemplate.getWhens()){
				rule.addCondition(when.getCondition(), when.getNamespaces());
			}
			rule.setPublishResults(actionTemplate.isPublish());
			if (logger.isDebugEnabled()) {
				if (actionTemplate.isPublish()){
					logger.debug("Rule " + rule.getName() + " has publish enabled.");
				}
			}
			ruleSet.addRule(rule);
		}
	}
	
	public void setRuleSet(RuleSet ruleSet){
		this.ruleSet = ruleSet;
	}

	public RuleSet getRuleSet() {
		return this.ruleSet;
	}
	
	@SuppressWarnings("unchecked")
	private NamespaceContext getContext(){
		return XPathUtil.getContext();
	}
	
	private XPathExpression createXPathExpression(String xpathExpression, NamespaceContext ctx) {		
		return XPathUtil.createXPathExpression(xpathExpression, ctx);
	}

	public Map<String, Object> getInputParameters() {
		return inputParameters;
	}

	@Override
	public boolean isConcurrent() {
		return this.concurrent;
	}

	/**
	 * used to turn conncurrent off even if the mashup xml is configured to run in parallel (for preview)
	 */
	@Override	
	public void setConcurrent(boolean parallel) {
		this.concurrent = parallel;		
	}
	
	public Set<ActionException> getParseErrors(){
		return Collections.<ActionException>unmodifiableSet(parseErrors);
	}

	@Override
	public Document getSchema() {
		return this.schema;
	}
	
	public Document getValidationRules() {
		return this.validationRules;
		
	}

    public Integer getRateLimit() {
        return rateLimit;
    }

    private void setRateLimit(Document document) {
        String value = document.getDocumentElement().getAttribute("rateLimit");
        if ( value != null ) {
            try {
                this.rateLimit = Integer.parseInt(value,10);
            }
            catch (NumberFormatException e) {
                this.rateLimit = null;
            }
        }
   	}

    public void setRateLimit(int rateLimit) {
        this.rateLimit = rateLimit;
    }
}
